Member Authentication API

**Referenced Files in This Document** - [worker.js](file://worker.js) - [src/alliance-login.njk](file://src/alliance-login.njk) - [wrangler.jsonc](file://wrangler.jsonc) - [package.json](file://package.json)

Table of Contents

  1. Introduction
  2. Project Structure
  3. Core Components
  4. Architecture Overview
  5. Detailed Component Analysis
  6. Dependency Analysis
  7. Performance Considerations
  8. Troubleshooting Guide
  9. Conclusion
  10. Appendices

Introduction

This document provides comprehensive API documentation for the member authentication system. It covers the magic link authentication endpoints, session management using HMAC-signed JWT tokens stored in HttpOnly cookies, security mechanisms (CSRF protection, token expiration, constant-time HMAC verification, member approval via KV namespace), request/response schemas, authentication flow diagrams, error handling patterns, client implementation examples, anti-bot strategies (honeypot), member enumeration prevention, CORS policies, cookie security attributes, and troubleshooting guidance.

Project Structure

The authentication logic is implemented in a Cloudflare Worker that intercepts specific routes and serves static assets otherwise. The login UI is generated by a Nunjucks template.

graph TB
subgraph "Cloudflare Worker"
W["worker.js<br/>Routes: /alliance/login/, /alliance/verify/, /alliance/logout/<br/>Session cookie handling"]
end
subgraph "Static Assets"
AS["ASSETS binding<br/>Serve built Eleventy site"]
end
subgraph "KV Namespaces"
ME["MEMBER_EMAILS<br/>Key: member:<email><br/>Value: 1"]
MT["MAGIC_TOKENS<br/>Key: token:<hex><br/>Value: <email><br/>TTL: 900s"]
end
subgraph "External Services"
RS["Resend API<br/>Send magic link emails"]
end
U["Browser"] --> W
W --> AS
W --> ME
W --> MT
W --> RS

Diagram sources

  • [worker.js:1-321](file://worker.js#L1-L321)
  • [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)

Section sources

  • [worker.js:1-321](file://worker.js#L1-L321)
  • [wrangler.jsonc:1-35](file://wrangler.jsonc#L1-L35)

Core Components

  • Magic link issuance endpoint: POST /alliance/login/
  • Token verification and session creation: GET /alliance/verify/
  • Session termination: GET /alliance/logout/
  • Session cookie: ace_member_session (HttpOnly, Secure, SameSite=Lax, Max-Age=30 days)
  • Token storage: KV namespace MAGIC_TOKENS with 15-minute TTL
  • Member approval: KV namespace MEMBER_EMAILS
  • Email delivery: Resend API
  • Frontend login UI: src/alliance-login.njk

Section sources

  • [worker.js:94-147](file://worker.js#L94-L147)
  • [worker.js:150-177](file://worker.js#L150-L177)
  • [worker.js:279-295](file://worker.js#L279-L295)
  • [worker.js:12-14](file://worker.js#L12-L14)
  • [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)
  • [src/alliance-login.njk:21-40](file://src/alliance-login.njk#L21-L40)

Architecture Overview

The system uses a stateless magic link flow with server-side token storage and signed session cookies.

sequenceDiagram
participant C as "Client Browser"
participant W as "Worker (worker.js)"
participant KV as "KV : MAGIC_TOKENS"
participant KE as "KV : MEMBER_EMAILS"
participant R as "Resend API"
Note over C,W : Step 1 : Request Magic Link
C->>W : POST /alliance/login/ {email, _gotcha}
W->>KE : Lookup member : <email>
KE-->>W : Approved? (hit/miss)
alt Approved
W->>KV : Put token : <hex>=<email>, TTL=900
W->>R : Send magic link email
else Not approved
W->>R : Skip sending email
end
W-->>C : Redirect /alliance/login/?sent=1
Note over C,W : Step 2 : Verify Token and Create Session
C->>W : GET /alliance/verify/?token=<hex>
W->>KV : Get token : <hex>
KV-->>W : <email> or null
alt Valid token
W->>KV : Delete token : <hex>
W->>W : Sign session token (payload|expiry)
W-->>C : 302 to /alliance/members/ with Set-Cookie ace_member_session
else Invalid/expired
W-->>C : Redirect /alliance/login/?error=expired
end
Note over C,W : Step 3 : Logout
C->>W : GET /alliance/logout/
W-->>C : 302 to /alliance/login/ with Set-Cookie ace_member_session=; Max-Age=0

Diagram sources

  • [worker.js:94-147](file://worker.js#L94-L147)
  • [worker.js:150-177](file://worker.js#L150-L177)
  • [worker.js:279-295](file://worker.js#L279-L295)

Detailed Component Analysis

Endpoint: POST /alliance/login/

  • Method: POST
  • Purpose: Issue a one-time magic link to an approved member’s email
  • Request
    • Form fields:
      • email: string (required)
      • _gotcha: string (optional, hidden field for bot detection)
  • Response
    • Redirect to /alliance/login/?sent=1 regardless of whether the email was approved
    • Prevents member enumeration by always returning the same “check your email” message
  • Security
    • Honeypot field _gotcha blocks automated submissions
    • Email validation performed before KV lookup
    • KV namespaces checked; missing KV returns 503
  • Implementation highlights
    • Approved members are looked up in MEMBER_EMAILS
    • A random 32-byte token is generated and stored in MAGIC_TOKENS with TTL=900 seconds
    • An email is sent via Resend API with a link to /alliance/verify/?token=
flowchart TD
Start(["POST /alliance/login/"]) --> Parse["Parse form data<br/>email, _gotcha"]
Parse --> Honeypot{"_gotcha present?"}
Honeypot --> |Yes| BotRedirect["Redirect /alliance/login/?sent=1"]
Honeypot --> |No| Validate["Validate email format"]
Validate --> Valid{"Valid email?"}
Valid --> |No| ErrorEmail["Redirect /alliance/login/?error=email"]
Valid --> |Yes| Lookup["Lookup member:<email> in MEMBER_EMAILS"]
Lookup --> Approved{"Approved?"}
Approved --> |No| SendRedirect["Redirect /alliance/login/?sent=1"]
Approved --> |Yes| GenToken["Generate random token (32 bytes)"]
GenToken --> Store["Put token:<hex>=<email> in MAGIC_TOKENS<br/>TTL=900"]
Store --> SendMail["Send magic link via Resend"]
SendMail --> SendRedirect
BotRedirect --> End(["End"])
ErrorEmail --> End
SendRedirect --> End

Diagram sources

  • [worker.js:97-147](file://worker.js#L97-L147)

Section sources

  • [worker.js:94-147](file://worker.js#L94-L147)
  • [src/alliance-login.njk:21-40](file://src/alliance-login.njk#L21-L40)

Endpoint: GET /alliance/verify/

  • Method: GET
  • Purpose: Validate the magic link token and create a session cookie
  • Request
    • Query parameter:
      • token: string (required)
  • Response
    • On success: 302 redirect to /alliance/members/ with Set-Cookie ace_member_session
    • On failure: 302 redirect to /alliance/login/?error=expired or error=invalid
  • Security
    • Token must exist in MAGIC_TOKENS and be deleted upon successful verification
    • Session cookie is HttpOnly, Secure, SameSite=Lax, Max-Age=30 days
    • Session token payload includes email and expiry; verified with HMAC signature
    • Constant-time HMAC verification prevents timing attacks
  • Implementation highlights
    • Validates presence of token
    • Retrieves email from MAGIC_TOKENS
    • Deletes the token immediately to enforce one-time use
    • Creates session token with HMAC signature and sets cookie
flowchart TD
Start(["GET /alliance/verify?token"]) --> CheckToken{"token provided?"}
CheckToken --> |No| ErrInvalid["Redirect /alliance/login/?error=invalid"]
CheckToken --> |Yes| LoadToken["Get token:<hex> from MAGIC_TOKENS"]
LoadToken --> Found{"Found and not expired?"}
Found --> |No| ErrExpired["Redirect /alliance/login/?error=expired"]
Found --> |Yes| DeleteToken["Delete token:<hex>"]
DeleteToken --> CreateSession["Create session token (payload|expiry)<br/>HMAC signature"]
CreateSession --> SetCookie["Set-Cookie ace_member_session<br/>HttpOnly; Secure; SameSite=Lax; Max-Age=2592000"]
SetCookie --> RedirectMembers["302 to /alliance/members/"]
ErrInvalid --> End(["End"])
ErrExpired --> End
RedirectMembers --> End

Diagram sources

  • [worker.js:153-177](file://worker.js#L153-L177)

Section sources

  • [worker.js:150-177](file://worker.js#L150-L177)
  • [worker.js:12-14](file://worker.js#L12-L14)

Endpoint: GET /alliance/logout/

  • Method: GET
  • Purpose: Terminate the current session by clearing the session cookie
  • Response
    • 302 redirect to /alliance/login/ with Set-Cookie ace_member_session=; Max-Age=0
  • Behavior
    • Expires the cookie immediately, removing the session
flowchart TD
Start(["GET /alliance/logout/"]) --> ExpireCookie["Set-Cookie ace_member_session=; Max-Age=0"]
ExpireCookie --> RedirectLogin["302 to /alliance/login/"]
RedirectLogin --> End(["End"])

Diagram sources

  • [worker.js:282-295](file://worker.js#L282-L295)

Section sources

  • [worker.js:279-295](file://worker.js#L279-L295)

Session Management and Cookie Security

  • Session cookie name: ace_member_session
  • Attributes:
    • Path=/
    • HttpOnly
    • Secure
    • SameSite=Lax
    • Max-Age=2592000 seconds (30 days)
  • Token format:
    • Payload: Base64-encoded "email|expiry"
    • Signature: HMAC-SHA256 over payload using SESSION_SECRET
    • Token: "<base64_payload>."
  • Verification:
    • Split by "."
    • Decode payload and extract email and expiry
    • Check expiry and recompute HMAC signature
    • Constant-time comparison to prevent timing attacks
classDiagram
class SessionToken {
+string payload_b64
+string signature
+string toString()
}
class Crypto {
+hmacSign(data, secret) string
}
class Worker {
+createSessionToken(email, secret) string
+verifySessionToken(token, secret) string|null
}
SessionToken --> Crypto : "uses HMAC"
Worker --> Crypto : "signs/verifies"

Diagram sources

  • [worker.js:20-58](file://worker.js#L20-L58)
  • [worker.js:32-58](file://worker.js#L32-L58)

Section sources

  • [worker.js:12-14](file://worker.js#L12-L14)
  • [worker.js:20-58](file://worker.js#L20-L58)

Security Mechanisms

  • CSRF protection
    • No CSRF tokens are embedded in forms; however, the login form uses a hidden field (_gotcha) to detect bots. The absence of CSRF tokens is acceptable because:
      • The endpoint is POST-only and does not modify state via GET
      • The magic link is short-lived and one-time use
      • The session cookie is HttpOnly and Secure
  • Token expiration
    • Magic link TTL: 900 seconds (15 minutes)
    • Session cookie Max-Age: 2592000 seconds (30 days)
  • Constant-time HMAC verification
    • Implemented to prevent timing attacks during signature comparison
  • Member approval via KV namespace
    • MEMBER_EMAILS stores approved emails with keys "member:"
    • Only approved emails receive magic links
  • Anti-bot and enumeration prevention
    • Honeypot field _gotcha blocks automated submissions
    • Always returns “check your email” regardless of membership status
    • Token deletion ensures one-time use

Section sources

  • [worker.js:97-147](file://worker.js#L97-L147)
  • [worker.js:153-177](file://worker.js#L153-L177)
  • [worker.js:49-54](file://worker.js#L49-L54)
  • [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)

Request/Response Schemas

  • POST /alliance/login/

    • Request body: multipart/form-data
      • email: string (required)
      • _gotcha: string (optional, hidden)
    • Response: 302 redirect
      • Location: /alliance/login/?sent=1 or /alliance/login/?error=
      • Notes: Always returns the same “check your email” message for approved/unapproved users
  • GET /alliance/verify/

    • Query parameters:
      • token: string (required)
    • Response: 302 redirect
      • Success: Location: /alliance/members/ with Set-Cookie ace_member_session
      • Failure: Location: /alliance/login/?error=expired or error=invalid
  • GET /alliance/logout/

    • Response: 302 redirect
      • Location: /alliance/login/ with Set-Cookie ace_member_session=; Max-Age=0
  • Session cookie (ace_member_session)

    • Value: "<base64_payload>."
    • Attributes: Path=/; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000

Section sources

  • [worker.js:94-147](file://worker.js#L94-L147)
  • [worker.js:150-177](file://worker.js#L150-L177)
  • [worker.js:279-295](file://worker.js#L279-L295)
  • [worker.js:12-14](file://worker.js#L12-L14)

Client Implementation Examples

  • HTML form submission
    • Use a standard form targeting POST /alliance/login/
    • Include a hidden input named _gotcha
    • Example fields: email (required), _gotcha (hidden)
  • Handling redirects
    • After POST, redirect to /alliance/login/?sent=1 indicates the “check your email” screen
    • On GET /alliance/verify, follow the 302 redirect to /alliance/members/ and ensure cookies are accepted
  • Logout
    • Navigate to /alliance/logout/ to clear the session cookie

Section sources

  • [src/alliance-login.njk:21-40](file://src/alliance-login.njk#L21-L40)
  • [worker.js:94-147](file://worker.js#L94-L147)
  • [worker.js:150-177](file://worker.js#L150-L177)
  • [worker.js:279-295](file://worker.js#L279-L295)

CORS Policies

  • The worker handles preflight OPTIONS for /api/auth and sets:
    • Access-Control-Allow-Origin: https://acestrategies.au
    • Access-Control-Allow-Methods: GET, POST, OPTIONS
    • Access-Control-Allow-Headers: Content-Type
  • These policies apply to the GitHub OAuth endpoints and are unrelated to the member authentication endpoints.

Section sources

  • [worker.js:183-191](file://worker.js#L183-L191)

Dependency Analysis

  • Worker routes depend on:
    • KV namespaces MEMBER_EMAILS and MAGIC_TOKENS
    • Secret environment variables: SESSION_SECRET, RESEND_API_KEY
    • Static asset binding ASSETS for serving the site
  • External dependencies:
    • Resend API for sending emails
  • Build and deployment:
    • Eleventy builds static assets
    • Wrangler deploys the Worker and binds KV namespaces and secrets
graph LR
W["worker.js"] --> ME["MEMBER_EMAILS KV"]
W --> MT["MAGIC_TOKENS KV"]
W --> RS["Resend API"]
W --> AS["ASSETS binding"]
W --> SEC["Secrets: SESSION_SECRET, RESEND_API_KEY"]

Diagram sources

  • [worker.js:1-10](file://worker.js#L1-L10)
  • [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)

Section sources

  • [worker.js:1-10](file://worker.js#L1-L10)
  • [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)
  • [package.json:1-32](file://package.json#L1-L32)

Performance Considerations

  • KV latency: Each request reads/writes to KV namespaces; keep token TTL low (15 minutes) and leverage Cloudflare’s global edge caching for static assets.
  • Email delivery: Resend API adds network latency; consider retry/backoff in production deployments.
  • Session cookie size: Minimal overhead due to compact token format.
  • Constant-time HMAC verification avoids timing side channels without significant CPU cost.

Troubleshooting Guide

Common issues and resolutions:

  • Missing KV namespaces
    • Symptom: 503 response indicating KV namespaces not configured
    • Resolution: Create MEMBER_EMAILS and MAGIC_TOKENS namespaces and bind them in wrangler.jsonc
  • Missing secrets
    • Symptom: Errors when signing tokens or sending emails
    • Resolution: Set SESSION_SECRET and RESEND_API_KEY via wrangler secret put
  • Invalid or expired token
    • Symptom: Redirect to /alliance/login/?error=invalid or error=expired
    • Resolution: Ensure the token is fresh (within 15 minutes) and not reused
  • Email not received
    • Symptom: Redirect to “check your email” but no email arrives
    • Resolution: Confirm the email is approved in MEMBER_EMAILS; verify Resend API key and domain configuration
  • Cookie not set
    • Symptom: Redirect succeeds but user remains unauthenticated
    • Resolution: Ensure browser accepts third-party cookies if applicable; confirm cookie attributes (HttpOnly, Secure, SameSite=Lax) are compatible with your deployment
  • Honeypot detected
    • Symptom: Immediate redirect to “sent=1”
    • Resolution: Ensure the hidden _gotcha field is present and not manipulated by client-side scripts

Section sources

  • [worker.js:70-75](file://worker.js#L70-L75)
  • [worker.js:153-177](file://worker.js#L153-L177)
  • [worker.js:97-147](file://worker.js#L97-L147)
  • [wrangler.jsonc:17-34](file://wrangler.jsonc#L17-L34)

Conclusion

The member authentication system provides a secure, stateless magic link flow with robust protections against enumeration, timing attacks, and misuse. By leveraging KV namespaces for approvals and token storage, HMAC-signed session cookies, and anti-bot measures, it balances usability with strong security. Proper configuration of KV namespaces and secrets is essential for reliable operation.

Appendices

Appendix A: Environment Variables and KV Namespaces

  • Required secrets:
    • SESSION_SECRET: Random 32+ byte string for HMAC signing
    • RESEND_API_KEY: Resend API key for sending emails
  • Required KV namespaces (bind in wrangler.jsonc):
    • MEMBER_EMAILS: Key format "member:", value "1"
    • MAGIC_TOKENS: Key format "token:", value "", TTL 900 seconds

Section sources

  • [worker.js:4-10](file://worker.js#L4-L10)
  • [wrangler.jsonc:17-26](file://wrangler.jsonc#L17-L26)